[C 기술면접] IPC와 공유메모리 — 종류·구현·동기화

NOTE

질문 1(IPC 설명·종류·구현)과 질문 4(공유메모리가 무엇이고 어떻게 구현하는가)를 묶었다. 핵심은 “프로세스는 주소 공간이 격리돼 있어서, 커널이 제공하는 통로를 거쳐야 데이터를 주고받을 수 있다” 는 것과, 공유메모리는 가장 빠르지만 동기화를 직접 해야 한다는 것이다.


Q1. IPC(Inter-Process Communication)란?

한 줄 답변

서로 다른 프로세스가 데이터를 주고받거나 실행 순서를 맞추기 위한 커널 제공 통신·동기화 메커니즘이다.

왜 필요한가

  • 프로세스마다 독립된 가상 주소 공간을 가져서, 한 프로세스가 다른 프로세스의 변수를 직접 읽을 수 없다(메모리 보호).
  • 그래서 커널이 중간에서 통로를 열어 줘야 한다. (스레드는 주소 공간을 공유하므로 IPC가 필요 없고 동기화만 필요하다.)
  • 프로세스를 나누는 이유: 장애 격리(한쪽이 죽어도 다른 쪽은 생존), 권한 분리, 모듈 독립 배포, 멀티코어 활용.

IPC 종류 비교

종류방향/형태특징대표 API적합한 용도
익명 파이프(pipe)단방향 바이트 스트림부모-자식(공통 조상) 관계에서만, 커널 버퍼(기본 64KB), 읽기/쓰기 블로킹pipe(), read/write셸 파이프라인(a | b), 부모-자식 간 간단한 통신
명명 파이프(FIFO)단방향 바이트 스트림파일 시스템에 이름이 있어 관계없는 프로세스도 사용mkfifo(), open단순한 프로세스 간 스트림
메시지 큐메시지 단위(경계 보존)타입별 수신 가능, 커널이 큐 유지System V: msgget/msgsnd/msgrcv/msgctl, POSIX: mq_open/mq_send/mq_receive비동기 메시지 전달, 우선순위 처리
공유메모리메모리 직접 공유가장 빠름(복사·시스템 콜 없음), 동기화는 직접System V: shmget/shmat, POSIX: shm_open + mmap대용량·고빈도 데이터 공유
세마포어동기화 도구데이터 전달이 아니라 접근 순서/개수 제어semget/semop, sem_open/sem_wait/sem_post공유메모리 보호, 자원 제한
소켓(Unix domain / TCP)양방향 스트림/데이터그램같은 호스트(UDS)와 다른 호스트(TCP/UDP) 모두 가능socket/bind/listen/accept/connect서버-클라이언트, 네트워크 분산
시그널비동기 알림데이터는 거의 못 실음(번호만), 신호 종류 제한kill, sigaction종료·재로드 통지
파일 / mmap 파일파일 기반영속적, 느림open, mmap설정·영속 데이터 공유
eventfd 등카운터/이벤트 fdepoll과 함께 사용 가능eventfd이벤트 루프에서 신호 전달

선택 기준

같은 호스트 · 대용량 · 최고 성능           → 공유메모리 + 동기화(세마포어/뮤텍스)
메시지 경계가 중요하고 비동기 전달          → 메시지 큐
단순한 부모-자식 스트림                    → 파이프
다른 호스트로 확장 가능성, 양방향 요청/응답   → 소켓(Unix domain socket은 같은 호스트에서 TCP보다 빠름)
단순 통지                                  → 시그널(데이터가 필요하면 다른 방식)

구현 예시 1) 파이프 (부모 → 자식)

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
 
int main(void) {
    int fd[2];                          /* fd[0]=읽기, fd[1]=쓰기 */
    if (pipe(fd) == -1) { perror("pipe"); return 1; }
 
    pid_t pid = fork();
    if (pid == 0) {                     /* 자식: 읽기 */
        close(fd[1]);                   /* 안 쓰는 쪽은 반드시 닫는다 */
        char buf[64];
        ssize_t n = read(fd[0], buf, sizeof(buf) - 1);
        if (n > 0) { buf[n] = '\0'; printf("child got: %s\n", buf); }
        close(fd[0]);
        _exit(0);
    }
    close(fd[0]);                       /* 부모: 쓰기 */
    const char *msg = "hello";
    write(fd[1], msg, strlen(msg));
    close(fd[1]);                       /* 닫아야 자식의 read가 EOF를 받는다 */
    waitpid(pid, NULL, 0);              /* 좀비 방지 */
    return 0;
}
  • 안 쓰는 끝을 닫지 않으면 read가 EOF를 못 받아 영원히 블로킹될 수 있다(자주 하는 실수).

구현 예시 2) System V 메시지 큐

struct msgbuf_t { long mtype; char mtext[128]; };
 
key_t key = ftok("/tmp/app", 'A');                 /* 두 프로세스가 같은 key 사용 */
int qid = msgget(key, IPC_CREAT | 0666);
 
/* 송신 */
struct msgbuf_t m = { .mtype = 1 };
strcpy(m.mtext, "hello");
msgsnd(qid, &m, sizeof(m.mtext), 0);
 
/* 수신: mtype==1인 메시지만 */
msgrcv(qid, &m, sizeof(m.mtext), 1, 0);
 
/* 삭제 (안 지우면 커널에 남는다: ipcs / ipcrm 으로 확인) */
msgctl(qid, IPC_RMID, NULL);

→ 자세한 API: 메시지 큐 API

구현 예시 3) Unix domain socket 서버 (요지)

int s = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un a = { .sun_family = AF_UNIX };
strncpy(a.sun_path, "/tmp/app.sock", sizeof(a.sun_path) - 1);
unlink(a.sun_path);
bind(s, (struct sockaddr*)&a, sizeof(a));
listen(s, 16);
int c = accept(s, NULL, NULL);       /* 이후 read/write */

꼬리 질문

질문답변 요지
IPC 자원이 프로세스 종료 후에도 남나?System V IPC(메시지 큐·세마포어·공유메모리)는 명시적으로 삭제하지 않으면 커널에 남는다. ipcs로 확인하고 ipcrm/*ctl(IPC_RMID)로 정리. 파이프·소켓 fd는 프로세스 종료 시 닫힌다.
System V vs POSIX IPC?System V: 오래된 표준, key 기반(ftok), API가 복잡. POSIX: 이름 기반(/name), 파일 디스크립터 스타일에 가깝고 API가 단순. 신규 개발은 POSIX 선호, 레거시는 System V가 많다.
파이프 vs 소켓?파이프는 단방향·같은 호스트·관계 필요(익명). 소켓은 양방향, 관계없는 프로세스·원격 호스트도 가능.
메시지 큐 vs 파이프?메시지 큐는 메시지 경계가 보존되고 타입/우선순위 선택 수신이 가능. 파이프는 경계 없는 바이트 스트림.
IPC로 대량 데이터 전달 시 성능이 병목이면?파이프/메시지 큐/소켓은 커널 복사 2회(유저→커널→유저)와 시스템 콜 비용이 있다. 공유메모리는 복사 없이 같은 물리 페이지를 공유한다.
IPC와 스레드 간 통신 차이?스레드는 같은 주소 공간이라 전역 변수·힙을 직접 공유(빠르지만 경쟁 조건 위험). 프로세스는 격리가 장점이지만 IPC 비용이 든다. (IPC와 스레드 메모리 공유 비교)

Q4. 공유메모리란? 어떻게 구현하나?

한 줄 답변

서로 다른 프로세스의 가상 주소 공간에 같은 물리 메모리 페이지를 매핑해, 복사 없이 같은 데이터를 읽고 쓰게 하는 IPC다.

구조

프로세스 A 가상 주소 0x7f00... ─┐
                                ├──→ 같은 물리 메모리(공유 세그먼트)
프로세스 B 가상 주소 0x7e90... ─┘     (가상 주소는 서로 달라도 됨)
  • 장점: 가장 빠르다(데이터 복사·시스템 콜 없음).
  • 단점: 커널이 동기화를 해 주지 않는다. 경쟁 조건, 일관성, 프로세스 사망 시 락 해제 문제를 직접 해결해야 한다.

구현 방법 A) System V 공유메모리

#include <sys/ipc.h>
#include <sys/shm.h>
 
struct shared { int count; char buf[256]; };
 
key_t key = ftok("/tmp/app", 'S');
int shmid = shmget(key, sizeof(struct shared), IPC_CREAT | 0666);   /* 생성/열기 */
struct shared *p = shmat(shmid, NULL, 0);                            /* 내 주소 공간에 붙이기 */
if (p == (void *)-1) { perror("shmat"); }
 
p->count++;                       /* 직접 읽고 쓰기 (동기화 필요!) */
 
shmdt(p);                         /* 떼어내기 */
shmctl(shmid, IPC_RMID, NULL);    /* 삭제 예약: 마지막 detach 후 제거 (한 곳에서만 호출) */

구현 방법 B) POSIX 공유메모리 + mmap (권장)

#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
 
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);    /* /dev/shm/my_shm */
ftruncate(fd, sizeof(struct shared));                    /* 크기 지정(필수) */
struct shared *p = mmap(NULL, sizeof(*p), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);                                               /* 매핑 후 fd는 닫아도 됨 */
 
/* ... 사용 ... */
 
munmap(p, sizeof(*p));
shm_unlink("/my_shm");                                   /* 이름 제거 (마지막에 한 번) */
  • 링크 시 -lrt가 필요한 환경이 있다(glibc 2.34 이전).
  • MAP_SHARED여야 다른 프로세스와 변경이 공유된다. MAP_PRIVATE는 copy-on-write로 공유되지 않는다.
  • 부모-자식 간이면 MAP_SHARED | MAP_ANONYMOUS로 fork 전에 매핑해도 된다.

반드시 알아야 할 동기화

공유메모리 단독으로는 race condition이 생긴다(count++도 읽기→더하기→쓰기).

방법설명
프로세스 공유 뮤텍스pthread_mutex_t를 공유메모리 안에 두고 PTHREAD_PROCESS_SHARED 지정. 소유자 사망 대비 PTHREAD_MUTEX_ROBUST
세마포어System V(semop, SEM_UNDO로 사망 시 복구) 또는 POSIX(sem_t pshared)
원자 연산카운터·플래그는 __atomic_*/C11 atomic으로 락 없이
락-프리 링 버퍼단일 생산자/단일 소비자(SPSC)라면 인덱스만 원자적으로 갱신
struct shared {
    pthread_mutex_t lock;        /* 값으로 포함 (포인터 X) */
    int count;
};
/* 생성자만 1번 초기화 */
pthread_mutexattr_t a;
pthread_mutexattr_init(&a);
pthread_mutexattr_setpshared(&a, PTHREAD_PROCESS_SHARED);
pthread_mutexattr_setrobust(&a, PTHREAD_MUTEX_ROBUST);
pthread_mutex_init(&p->lock, &a);

공유메모리 설계 시 함정

함정설명과 대응
포인터를 저장하면 안 됨가상 주소가 프로세스마다 다르다. 구조체 안에서는 오프셋/인덱스를 쓴다.
구조체 레이아웃 불일치패딩·정렬이 모든 프로세스에서 같아야 한다(같은 헤더, 같은 컴파일 옵션). 가변 길이 타입(long, size_t) 주의, 고정폭 타입(uint32_t) 권장
락 보유 중 프로세스 사망영원히 대기 → robust mutex(EOWNERDEAD) 또는 SEM_UNDO
초기화 경쟁누가 먼저 생성/초기화하는지 정해야 한다(생성자 1개, 또는 O_EXCL 로 생성 성공한 쪽이 초기화)
자원 누수shm_unlink/IPC_RMID 누락 시 /dev/shm 또는 ipcs에 잔존
읽기만 해도 위험여러 필드를 읽는 도중 쓰기가 일어나면 torn read → 락 또는 시퀀스 번호(seqlock)
캐시 일관성/메모리 순서원자 연산·메모리 배리어 없이 플래그로만 동기화하면 재배치로 깨질 수 있다

꼬리 질문

질문답변 요지
공유메모리가 왜 가장 빠른가?데이터 복사와 시스템 콜이 없다. 초기 매핑 이후에는 일반 메모리 접근과 같다.
mmap과 공유메모리의 관계?공유메모리는 mmap(MAP_SHARED)로 구현된다. 파일을 매핑하면 파일 기반 공유(영속), shm_open 객체를 매핑하면 메모리 기반 공유.
System V 공유메모리 삭제 시점?IPC_RMID는 “삭제 예약”이다. attach된 프로세스가 모두 detach해야 실제로 제거된다.
공유메모리 + 세마포어 대신 메시지 큐를 쓰는 경우?동기화와 경계가 필요한 소량 메시지는 큐가 간단하다. 대용량·고빈도는 공유메모리.
스레드의 전역 변수와 뭐가 다른가?스레드는 같은 주소 공간이라 포인터를 그대로 공유할 수 있다. 프로세스 간 공유메모리는 가상 주소가 달라 포인터를 못 쓴다.

관련 문서